Near-Plane Clipping - Sixth 3D
Table of Contents
1. The problem
When the camera brushes against geometry — a floor tile under your
feet, a wall you lean into — part of a polygon can end up behind the
viewer while the rest stays in front. Perspective projection divides by
depth (screenX = x / z), so a vertex at z ≤ 0 has no meaningful screen
position at all.
The naive way out — dropping any polygon that has even one vertex behind the camera — makes whole tiles vanish exactly when they are closest and largest on screen. Walking through the House demo, floor tiles blinked out of existence at the bottom of the frame:
Sixth 3D instead clips the polygon against the near plane and renders the surviving fragment. The same frame with clipping enabled — the floor is solid to the bottom edge:
Think of the camera plane as the edge of a table and the polygon as a sheet of paper partly hanging off it. Dropping the polygon means throwing away the whole sheet. Clipping takes scissors, cuts the sheet along the table edge, and keeps the part that lies on the table.
2. Why not just clamp z?
A tempting one-liner is to force every vertex to z = max(z, epsilon)
and project anyway. It fails geometrically: a vertex at z = −50 clamped
to z = 0.01 projects to a screen coordinate thousands of pixels away,
in the wrong direction — the sign flip of the division mirrors it
through the camera. The polygon smears into giant streaks across the
frame instead of ending cleanly at the screen edge.
Clipping produces the geometrically correct cut: the polygon's new edge lies exactly on the near plane, and everything the rasterizer receives has z > 0.
3. How the clipping works
Clipping happens in camera space, after the transform stack has moved
vertices relative to the viewer but before the perspective divide.
The vertex loop is walked edge by edge (Sutherland-Hodgman style)
against the plane z = nearPlaneDistance:
- An in-front vertex passes through unchanged.
- An edge that crosses the plane spawns a new vertex at the
intersection, with position, UV and normal all interpolated with the
same parameter
t. - A behind-plane vertex is skipped.
Interpolating UVs linearly along the 3D edge is exactly right for the perspective-correct texture mapper: the intersection vertex is a real point on the original edge, so its texture coordinate is the same blend of the endpoints' UVs. Textured fragments therefore show the correct texels right up to the cut, with no seam.
Only a polygon with all vertices behind the plane is culled — the legitimate version of the old behavior.
4. From clipped loop to pixels
A convex N-gon crossing the plane clips to a single contiguous loop of at most N+1 vertices. For the triangle-based rasterizers this means a triangle can become a quad, which is painted as a two-triangle fan sharing the first vertex — exact, because the clip of a convex polygon stays convex:
Shape support:
| Shape | Behavior when straddling |
|---|---|
| SolidPolygon | Clipped loop painted as triangle fan |
| TexturedTriangle | Fan-painted with interpolated UVs (also inherited by lightmapped GI fragments) |
| Line | Shortened to the in-front endpoint + intersection |
| Billboard | Single anchor point: culled when behind, as before |
Implementation notes:
- Clipped output is stored per pipeline slot on the shape
(
clippedVertices(ctx)), so the triple-buffered pipeline can transform frame N+1 while frame N is still painting. - Depth sorting and tile binning use the clipped vertices' average Z and screen bounds — a clipped tile sorts as the fragment it became, not as the polygon that reached behind you.
- New intersection vertices exist only in camera space; they are projected directly via Vertex.setCameraSpaceCoordinate(), bypassing the transform stack.
5. Configuration
The near plane distance is a per-context knob, in world units:
// Default is 1.0; smaller values let the camera press closer to // geometry before the scissors bite, at the cost of larger projected // coordinates for clipped fragments. viewPanel.getRenderingContext().nearPlaneDistance = 0.5;